package ifm4j.collections.treeCollections;

//import ifm4j.collections.treeCollections.arguments.TreeCollectionArgs;
//import ifm4j.collections.treeCollections.arguments.TreeObjectAddedArgs;
//import ifm4j.collections.treeCollections.arguments.TreeObjectCreatedArgs;
import ifm4j.collections.treeCollections.factories.TreeFactory;
import ifm4j.collections.treeCollections.factories.TreeFactoryTable;
import ifm4j.collections.treeCollections.factories.TreeObjectFactory;
import ifm4j.collections.treeCollections.factories.TreeRootFactory;
import ifm4j.utils.Identifier;
import ifm4j.utils.InvalidIdentifierException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


public abstract class Tree {
	private static final String XML_CLASS = "Class";

    private static DocumentBuilder xmlDocBuilder;

    protected Document xmlDocument;

//    protected EventHandler<Object, TreeCollectionArgs> treeModifyCompleted;

//    protected EventHandler<Object, TreeCollectionArgs> treeModifyBefore;

    protected TreeRoot root;

    protected boolean dirty;

    protected TreeFactory treeFactory;

    protected Hashtable<Identifier, TreeObject> objStorage;
    static {
        try {
            xmlDocBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            xmlDocBuilder = null;
        }
    }

    /**
     * Construct Tree
     * 
     * @author Shirley
     */
    public Tree() {
        this.xmlDocument = xmlDocBuilder.newDocument();
        this.treeFactory = TreeFactoryTable.getFactoryTable().getFactory(this.getClass().getName());

        this.dirty = false;
        // storage
        this.objStorage = new Hashtable<Identifier, TreeObject>();
        // generate root
        root = this.constructTreeRoot();
    }

    /**
     * Construct Tree with tree root
     * 
     * @author Shirley
     * @param root
     */
    public Tree(TreeRoot root) {
        this.xmlDocument = xmlDocBuilder.newDocument();
        this.treeFactory = TreeFactoryTable.getFactoryTable().getFactory(
                this.getClass().getName());

        this.dirty = false;
        this.root = root;
        objStorage = new Hashtable<Identifier, TreeObject>();

        // add all tree objects into tree
        Collection<TreeObject> allObjects = getAllTreeObject(root);
        for (TreeObject tmpObj : allObjects) {
            objStorage.put(tmpObj.getIdentifier(), tmpObj);
        }
    }

    /**
     * Get XML document.
     * 
     * @return
     */
    protected Document getXmlDocument() {
        return this.xmlDocument;
    }

    /**
     * Get Name of tree. the name is for XML
     * 
     * @author Shirley
     * @return
     */
    public String getName() {
        String fullClassName = this.getClass().getName();
        int dotLoc = fullClassName.lastIndexOf('.');
        if (dotLoc == -1) {
            return fullClassName;
        }

        return fullClassName.substring(dotLoc + 1);
    }

    /**
     * <p>
     * get tree root
     * </p>
     * <p>
     * Cannot be override
     * </p>
     * 
     * @author Shirley
     * @return
     */
    public final TreeRoot getTreeRoot() {
        return this.root;
    }

    /**
     * <p>
     * All tree objects including tree node, tree root, tree leaf.
     * </p>
     * <p>
     * Cannot be override
     * </p>
     * 
     * @author Shirley
     * @return
     */
    public final Collection<TreeObject> getAllTreeObjects() {
        return this.objStorage.values();
    }

    /**
     * <p>
     * Get tree object based on the index
     * </p>
     * <p>
     * Cannot be override
     * </p>
     * 
     * @author Shirley
     * 
     * @param index
     * @return
     */
    public final TreeObject getTreeObject(Identifier index) {
        return this.objStorage.get(index);
    }

    /**
     * <p>
     * Get tree object based on the index
     * </p>
     * <p>
     * Cannot be override
     * </p>
     * 
     * @author Shirley
     * @param index
     * @return
     * @throws TreeCollectionException
     */
    public final TreeObject getTreeObject(String index)
            throws TreeCollectionException {
        try {
            Identifier id = new Identifier(index);
            return this.getTreeObject(id);
        } catch (InvalidIdentifierException e) {
            throw new TreeCollectionException("Invalid Identifier.");
        }
    }

    /**
     * <p>
     * It means dirty when the tree has been modified since last operation.
     * </p>
     * <p>
     * Cannot be override.
     * </p>
     * 
     * @author Shirley
     * @return
     */
    public final boolean isDirty() {
        return this.dirty;
    }

    /**
     * <p>
     * Get count of all objects in the tree
     * </p>
     * <p>
     * Cannot be override
     * </p>
     * 
     * @author Shirley
     * @return
     */
    public final int getTreeObjectsCount() {
        return this.objStorage.size();
    }

    /**
     * <p>
     * contains the tree object, same instance.
     * </p>
     * <p>
     * This method cannot override,
     * </p>
     * 
     * @author Shirley
     * @param treeObject
     * @return
     */
    public final boolean contains(TreeObject treeObject) {
        for (TreeObject obj : objStorage.values()) {
            // reference equal
            if (obj == treeObject) {
                return true;
            }
        }
        return false;
    }

    /**
     * <p>
     * Generate Tree Node Identity, make sure that non-repeated id in the tree.
     * </p>
     * <p>
     * This method cannot override,
     * </p>
     * 
     * @author Shirley
     * @return
     */
    public final Identifier generateId() {
        Identifier newId = new Identifier();
        return newId;
    }

    /**
     * <p>
     * Construct Tree Root
     * </p>
     * 
     * @author Shirley
     * @return
     */
    protected TreeRoot constructTreeRoot() {
        Identifier id = this.generateId();
        TreeRootFactory rootFactory = this.treeFactory.getTreeRootFactory();
        TreeRoot root = (TreeRoot) rootFactory.constructNewInstance(id, null);

        root.setTreeOwner(this);
        this.objStorage.put(id, root);
        return root;
    }

    /**
     * <p>
     * Construct new instance with the arguments input, and then add into tree
     * under parent.
     * </p>
     * <p>
     * The methods cannot override
     * </p>
     * 
     * @author Shirley
     * @param className
     *            class name of new instance
     * @param parent
     *            parent of new instance
     * @param args
     *            arguments
     * @return
     * @throws TreeCollectionException
     */
    public final TreeObject createNewInstance(String className, INonLeaf parent, Object... args) throws TreeCollectionException {
//        TreeObjectCreatedArgs argu = new TreeObjectCreatedArgs(this, parent, className, args);

        // do before process
//        this.beforeProcess(this, argu);

        // generate id
        Identifier id = this.generateId();
        TreeObjectFactory factory = this.treeFactory.getObjectFactory(className);
        if (factory == null) {
            throw new TreeCollectionException("You should register the tree object factory at first.");
        }

        TreeObject newInstance = factory.constructNewInstance(id, args);
        if (!(newInstance instanceof INonRoot)) {
            throw new TreeCollectionException("Cannot create new tree root instance.");
        }

        subAddInstance(parent, (INonRoot) newInstance);

        // after process

//        this.afterProcess(this, argu);
        return newInstance;
    }

    /**
     * <p>
     * Add instance into the current tree. Make sure that the instance not add
     * into any tree yet. And id of the instance does not exist in current tree.
     * </p>
     * <p>
     * The methods cannot override.
     * </p>
     * 
     * @author Shirley
     * @param parent
     * @param instance
     * @throws TreeCollectionException
     */
    public final void addInstance(INonLeaf parent, INonRoot instance)
            throws TreeCollectionException {
//        TreeObjectAddedArgs args = new TreeObjectAddedArgs(this, parent, instance);

//        this.beforeProcess(this, args);

        if (!(instance instanceof TreeObject)) {
            throw new TreeCollectionException(
                    "The tree object to add in is not Class 'TreeObject'");
        }
        TreeObject newObject = (TreeObject) instance;

        if (this.contains(newObject)) {
            throw new TreeCollectionException(
                    "The tree object already existed in the tree.");
        }

        if (instance.getParent() != null || newObject.getTreeOwner() != null) {
            throw new TreeCollectionException(
                    "The tree object already has parent or was in the tree.");
        }

        this.subAddInstance(parent, instance);
//        this.afterProcess(this, args);
    }

    /**
     * Subclass of 'addInstance' which do the operations and can be override.
     * 
     * @author Shirley
     * @param parent
     * @param instance
     */
    protected final void subAddInstance(INonLeaf parent, INonRoot instance) {
        parent.addChild(instance);
        // add to current tree storage
        TreeObject treeObj = (TreeObject) instance;
        treeObj.setTreeOwner(this);
        this.objStorage.put(treeObj.getIdentifier(), treeObj);
    }

    /**
     * <p>
     * Delete exist Instance. Make sure that the instance is one of the objects
     * of the tree. The 'delete' operation will delete the tree object and all
     * the tree from it.
     * </p>
     * <p>
     * The method cannot be override.
     * </p>
     * 
     * @author Shirley
     * @param treeObject
     */
    public final void deleteExistInstance(TreeObject treeObject) {

    }

    /**
     * Subclass of 'deleteExistInstance' which do the operations and can be
     * override.
     * 
     * @author Shirley
     * @param treeObject
     */
    protected void subDeleteExistInstance(TreeObject treeObject) {

    }

//    /**
//     * ProProcess(object sender, TreeRefreshEventArgs e)
//     * <p>
//     * Process after tree operations.
//     * </p>
//     * <p>
//     * The method cannot override.
//     * </p>
//     */
//    public final void afterProcess(Object sender, TreeCollectionArgs e) {
//
//    }

//    /**
//     * PreProcess(object sender, TreeRefreshEventArgs e)
//     * <p>
//     * Process before tree operations.
//     * </p>
//     * <p>
//     * The method cannot override.
//     * </p>
//     */
//    public final void beforeProcess(Object sender, TreeCollectionArgs e) {
//
//    }

    /**
     * Parse to XML document.
     * 
     * @return
     * @throws TreeCollectionException
     * @throws DOMException
     */
    public Document toXDoc() throws DOMException, TreeCollectionException {
        Element xelem = this.toXElem();
        this.xmlDocument.appendChild(xelem);
        return this.xmlDocument;
    }

    /**
     * Parse XML document to tree
     * 
     * @param type
     * @param doc
     * @return
     * @throws TreeCollectionException
     */
    public static Tree xdocParser(Document doc) throws TreeCollectionException {
        Element root = doc.getDocumentElement();
        return xelemParser(root);
    }

    /**
     * Parse to XML element
     * 
     * @return
     * @throws TreeCollectionException
     * @throws DOMException
     */
    public Element toXElem() throws DOMException, TreeCollectionException {
        String name = this.getName();
        Element xelem = this.xmlDocument.createElement(name);
        xelem.appendChild(root.toXElem());
        xelem.setAttribute(XML_CLASS, this.getClass().getName());
        return xelem;
    }

    /**
     * Parse XML element to tree
     * 
     * @param elem
     * @return
     * @throws TreeCollectionException
     */
    public static Tree xelemParser(Element elem) throws TreeCollectionException {
        String className = elem.getAttribute(XML_CLASS);

        TreeFactory treeFactory = TreeFactoryTable.getFactoryTable()
                .getFactory(className);
        if (treeFactory == null) {
            throw new TreeCollectionException(
                    "Invalid (XML) class name of the tree or the tree factory does not register in the tree factory table.");
        }
        TreeRootFactory rootTreeFactory = treeFactory.getTreeRootFactory();
        NodeList nodeList = elem.getChildNodes();
        Element rootElement = null;

        // get the tree root element
        for (int i = 0; i < nodeList.getLength(); ++i) {
            Node node = nodeList.item(i);
            if (!(node instanceof Element)) {
                continue;
            }
            Element tmpElem = (Element) node;
            String tmpClassName = tmpElem.getAttribute(XML_CLASS);
            if (rootTreeFactory.getTreeClassName().equals(tmpClassName)) {
                if (rootElement != null) {
                    throw new TreeCollectionException("Invalid XML: One more tree root in the XML.");
                }
                rootElement = tmpElem;
            }
        }

        if (rootElement == null) {
            throw new TreeCollectionException("Invalid XML: No tree root in the XML.");
        }
        TreeRoot tmpRoot = (TreeRoot) TreeObject.xmlParser(rootElement,
                treeFactory);
        Tree tree = treeFactory.ConstructTreeWithRoot(tmpRoot, null);
        treeFactory.xmlParser(elem, tree);

        return tree;
    }

    /**
     * @author Shirley
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Tree)) {
            return false;
        }
        Tree tree = (Tree) obj;

        return this.treeHierarchyEquals(tree);
    }

    /**
     * Equals on tree hierarchy. Not on tree's parameter.
     * 
     * @param tree
     * @return
     */
    public boolean treeHierarchyEquals(Tree tree) {
        // check valid
        return this.objStorage.equals(tree.objStorage);
    }

    /**
     * @author Shirley
     */
    @Override
    public Object clone() {
        return null;
    }

    /**
     * Get all tree object from the root
     * 
     * @param root
     * @return
     */
    private static Collection<TreeObject> getAllTreeObject(TreeRoot root) {
        int curObj = 0;
        ArrayList<TreeObject> set = new ArrayList<TreeObject>();
        set.add(root);

        for (curObj = 0; curObj < set.size(); ++curObj) {
            TreeObject tmpObj = set.get(curObj);
            if (!(tmpObj instanceof INonLeaf)) {
                continue;
            }
            INonLeaf nonLeaf = (INonLeaf) tmpObj;
            for (INonRoot child : nonLeaf.getChildren()) {
                set.add((TreeObject) child);
            }
        }

        return set;
    }
}
